package net.shambolica.jport.runner;

import org.apache.log4j.*;
import org.apache.log4j.varia.NullAppender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.PrintStream;
import java.util.Map;
import java.util.LinkedHashMap;

/**
 * I implement a JPort on stdin/out, with err going to a null stream.
 * The caller should simply do System.exit(runner.run()).
 * No cookie/expected combo is used.
 * TODO: use env var JPORT_ERR as output file name.
 */
public abstract class JPortSimpleRunner {

    protected final Logger log;

    /**
     * Constructor: provide logger here.
     * @param log Logger.
     */
    protected JPortSimpleRunner(Logger log) {
        this.log = log;
    }


    /**
     * Simpler constructor - just provide the runner's class for logger factory.
     * @param runnerClass Class of actual logic container.
     */
    protected JPortSimpleRunner(Class runnerClass) {
        this.log = LoggerFactory.getLogger(runnerClass);
    }


    /**
     * Getter for logger.
     * @return The runner's logger.
     */
    public Logger getLogger() {
        return log;
    }

    
    /**
     * Abstract method - implement this in subclasses to provide FUNCTIONALITY.
     * @param io    Data connection, running on stdio.
     * @return      Application exit status.
     */
    protected abstract int run(DataConnection io) throws IOException, InterruptedException;


    /**
     * This method runs the runner, and returns an exit status.
     * @return The application/process exit status.
     */
    public int run() {
        System.setErr(new PrintStream(new NullOutputStream()));
        log.info("creating stdio etf data connection");
        DataConnection io = new EtfStreamConnection(System.in, System.out);
        log.info("running application");
        int exitStatus = 0;
        try {
            exitStatus = run(io);
        } catch (IOException e) {
            log.info("io exception encountered: {}: {}", e.getClass().getName(), e.getMessage());
            exitStatus = JPortNetRunner.STATUS_ABNORMAL_TERMINATION;
        } catch (InterruptedException e) {
            log.info("interrupted: {}: {}", e.getClass().getName(), e.getMessage());
            exitStatus = JPortNetRunner.STATUS_ABNORMAL_TERMINATION;
        }
        log.info("exit status = {}", exitStatus);
        return exitStatus;
    }


    /**
     * A utility function, for initializing the general logging appenders.
     * Logs would be discarded (when filename is empty), or written to file 'filename'.
     * If the pattern is provided, it will be used in case of file logging.
     * @param filename      If not null, logging will be done to the specified file.
     * @param pattern       If not null, the specified pattern will be used during
     *                      file logging. If null, a default pattern will be used.
     * @param level
     * @throws IOException  If log file cannot be created/appended to.
     */
    public static void initLog(String filename, String pattern, Level level) throws IOException {
        if (pattern == null) {
            pattern = "%d{ABSOLUTE} %5p %c{1},%t:%L - %m%n";
        }
        Appender logAppender = filename == null || filename.length() == 0 ?
                new NullAppender() : 
                new FileAppender(new PatternLayout(pattern), filename);
        BasicConfigurator.configure(logAppender);
        org.apache.log4j.Logger.getRootLogger().setLevel(level != null ? level : getLogLevelFromEnv());
    }


    private static final Map<String, Level> levels = new LinkedHashMap<String, Level>();
    static {
        levels.put("off", Level.OFF);
        levels.put("no", Level.OFF);
        levels.put("false", Level.OFF);
        levels.put("all", Level.ALL);
        levels.put("trace", Level.TRACE);
        levels.put("debug", Level.DEBUG);
        levels.put("info", Level.INFO);
        levels.put("yes", Level.INFO);
        levels.put("true", Level.INFO);
        levels.put("warn", Level.WARN);
        levels.put("warning", Level.WARN);
        levels.put("err", Level.ERROR);
        levels.put("error", Level.ERROR);
        levels.put("fatal", Level.FATAL);
        levels.put("critical", Level.FATAL);
    }


    /**
     * Extract default log level from environment variable "JPORT_LOG_LEVEL".
     * If none such exists, the level will be OFF.
     * @return Default logging level.
     */
    public static Level getLogLevelFromEnv() {
        String envLevel = System.getenv("JPORT_LOG_LEVEL");
        envLevel = envLevel == null ? "off" : envLevel.toLowerCase();

        return levels.containsKey(envLevel) ? levels.get(envLevel) : levels.get("off");
    }


    /**
     * Test main.
     * Please model your app mains closely after this - any logging data
     * that reaches stdout or stderr will corrupt the data connection.
     * @param arg arguments, ignored.
     * @throws java.io.IOException if FileAppender cannot be created.
     */
    public static void main(String[] arg) throws IOException {
        // Set up logging/er.
        initLog("c:/tmp/log.out", null, Level.INFO);

        // Create and run runner.
        JPortSimpleRunner runner = new ATestRunner();
        Logger log = runner.getLogger();
        log.info("running the runner"); // Should not show.
        int exitStatus = runner.run();
        log.info("status={}", exitStatus); // Should not show.

        // Done, exit with proper status.
        System.exit(exitStatus);
    }


    private static class ATestRunner extends JPortSimpleRunner {
        private ATestRunner() {
            super(ATestRunner.class);
        }

        protected int run(DataConnection io) {
            log.info("here's to loggin ya!");
            return 0;
        }
    }

}
